package main

import (
	"errors"
	"fmt"
	"gitee.com/kinwyb/appTools/manager"
	"github.com/shirou/gopsutil/process"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"
)

var runProcess *Process //当前运行中了的进程

var serverVersion, appName, tag string //服务器运行版本

var processStartLock = sync.Mutex{}

type ProcessConfig struct {
	Name     string      `description:"进程名称,对应的就是运行文件的名称"`
	Args     []string    `description:"运行参数"`
	Tag      string      `description:"标记,用于区分名称相同进程"`
	Ver      string      `description:"版本信息"`
	id       string      `description:"组合出来的ID"`
	doneChan chan string `description:"结束通知"`
}

func createProcessConfig(name string, tag string, args []string) *ProcessConfig {
	ret := &ProcessConfig{
		Name:     name,
		Tag:      tag,
		id:       name,
		Args:     args,
		doneChan: make(chan string, 1),
	}
	ret.init()
	return ret
}

func (p *ProcessConfig) init() {
	p.id = p.Name + "-" + getClientID()
	go processErrHandler(p.doneChan)
}

type Process struct {
	ID           string
	IsStart      bool
	startTime    int64
	cfg          *ProcessConfig
	cmd          exec.Cmd
	processInfo  *process.Process
	lockFilePath string
	stopSignal   chan bool //结束信号
}

// 新进程
func newProcess(cfg *ProcessConfig) (*Process, error) {
	if cfg.Name == "" {
		return nil, errors.New("进程名不能为空")
	}
	ret := &Process{
		ID:         cfg.id,
		IsStart:    false,
		startTime:  0,
		cfg:        cfg,
		stopSignal: make(chan bool),
	}
	ret.lockFilePath = filepath.Join(defaultAppDir, ret.ID+".lock")
	ret.initCmd()
	return ret, nil
}

// 初始化cmd
func (p *Process) initCmd() {
	p.startTime = 0
	p.processInfo = nil
	// 应用启动设置
	p.cmd = exec.Cmd{
		Path:   p.ID,
		Dir:    defaultAppDir,
		Args:   append([]string{p.ID}, p.cfg.Args...),
		Stdout: manager.ServiceLog.Writer(),
		Stderr: manager.ServiceLog.Writer(),
	}
}

// 获取软件版本
func (p *Process) version() string {
	if p == nil {
		return ""
	}
	c := exec.Command("./"+p.ID, "version")
	c.Dir = defaultAppDir
	data, _ := c.Output()
	version := string(data)
	return strings.TrimSpace(version)
}

// Start 启动进程
func (p *Process) Start() error {
	if p == nil {
		return errors.New("Process is nil ")
	}
	if p.IsStart {
		return nil
	}
	_, e := os.Stat(filepath.Join(defaultAppDir, p.cfg.id))
	if os.IsNotExist(e) { //不存在运行文件
		_, e := os.Stat(filepath.Join(defaultAppDir, p.cfg.Name))
		if os.IsNotExist(e) {
			// 检测运行文件不存在要从服务端下载
			if serverVersion == "" { //获取不到服务器版本
				manager.ServiceLog.Error("服务端运行运行版本获取失败")
				return errors.New("服务端运行运行版本获取失败")
			}
			err := downExecFile(p.cfg.Name, serverVersion)
			if err != nil {
				manager.ServiceLog.WithField("info", p.cfg.Name).WithError(err).Error("从服务的下载执行文件失败")
				return errors.New("从服务的下载执行文件失败")
			}
		}
		// 原文件复制一份根据id重命名
		cp(p.cfg.Name, p.cfg.id)
	}
	p.cfg.Ver = p.version()
	p.IsStart = true
	p.startTime = time.Now().Unix()
	manager.ServiceLog.Info("程序运行开始")
	// 开始运行
	go func() {
		errMsg := ""
		strb, _ := ioutil.ReadFile(p.lockFilePath)
		pid, err := strconv.ParseInt(string(strb), 10, 64)
		if err == nil && pid > 1 {
			process, err := os.FindProcess(int(pid))
			if err != nil {
				manager.ServiceLog.WithField("info", fmt.Sprintf("ID:%s pid:%d", p.ID, pid)).WithError(err).Warn("结束进程失败")
			} else {
				err := process.Kill()
				// 不是进程已结束的错误记录日志
				if err != nil && err.Error() != "os: process already finished" {
					manager.ServiceLog.WithField("info", fmt.Sprintf("ID:%s pid:%d", p.ID, pid)).WithError(err).Warn("结束进程失败")
				} else if err == nil {
					manager.ServiceLog.WithField("info", fmt.Sprintf("ID:%s pid:%d", p.ID, pid)).Warn("历史进程结束")
				}
			}
		}
		err = p.cmd.Start()
		if err == nil {
			processStartLock.Lock()
			ioutil.WriteFile(p.lockFilePath, []byte(fmt.Sprintf("%d", p.cmd.Process.Pid)), 0666)
			processStartLock.Unlock()
			err = p.cmd.Wait()
		}
		if p.IsStart {
			p.IsStart = false
			if err != nil {
				errMsg = err.Error()
			}
		} else { //结束信号
			p.stopSignal <- true
		}
		p.initCmd()
		p.cfg.doneChan <- fmt.Sprintf("%s:%s", p.ID, errMsg) //告知进程运行停止
	}()
	return nil
}

// Stop 停止进程
func (p *Process) Stop() error {
	if p == nil {
		return errors.New("Process is nil ")
	}
	p.startTime = 0
	if !p.IsStart {
		return nil
	}
	p.IsStart = false
	manager.ServiceLog.Info("程序运行结束")
	err := p.cmd.Process.Kill()
	if err != nil {
		manager.ServiceLog.WithError(err).Error("进程停止失败")
		return err
	}
	<-p.stopSignal
	processStartLock.Lock()
	os.Remove(p.lockFilePath)
	processStartLock.Unlock()
	return nil
}

// Kill 结束进程
func (p *Process) Kill() {
	p.cmd.Process.Kill()
}

// ProcessUseMemAndCpu 获取内存和cpu数据
func (p *Process) ProcessUseMemAndCpu() (float64, float64) {
	if p.processInfo == nil {
		if p.cmd.Process == nil {
			return -1, -1
		}
		process, err := process.NewProcess(int32(p.cmd.Process.Pid))
		if err != nil {
			manager.ServiceLog.WithError(err).Error("内存cpu数据进程信息获取失败")
			return -1, -1
		}
		p.processInfo = process
	}
	if p.processInfo == nil {
		return -1, -1
	}
	useMem := 0.0
	useCpu := 0.0
	mem, err := p.processInfo.MemoryInfo()
	if mem == nil || err != nil {
		useMem = -1
	} else {
		useMem = float64(mem.RSS) / 1048576 //转换成m (1024*1024)
	}
	cpu, err := p.processInfo.CPUPercent()
	if err != nil {
		useCpu = -1
	} else {
		useCpu = cpu
	}
	return useMem, useCpu
}

// 进程停止处理
func processErrHandler(processDownChan <-chan string) {
	defer func() {
		if err := recover(); err != nil {
			manager.ServiceLog.WithField("error", err).Error("进程错误处理线程崩溃")
			go processErrHandler(processDownChan) //重新开启
		}
	}()
	for v := range processDownChan {
		vs := strings.SplitN(v, ":", 2)
		// 告知服务端运行停止
		if vs[1] == "" {
			manager.ServiceLog.WithField("info", vs[0]).Info("进程正常停止")
		} else {
			manager.ServiceLog.WithField("info", vs).Error("进程异常停止")
			time.Sleep(5 * time.Second) //5秒后重启
			// 异常结束自动重新启动
			runProcess.Start()
		}
	}
}

// 初始化进程信息
func initProcess() {
	cfg := createProcessConfig(appName, tag, nil)
	cfg.init()
	var err error
	runProcess, err = newProcess(cfg)
	if err != nil {
		runProcess = nil
		manager.ServiceLog.WithField("info", cfg).WithError(err).Fatal("运行异常")
	}
}

// 拷贝文件,src原文件，dst目标文件
func cp(src string, dst string) {
	src = filepath.Join(defaultAppDir, src)
	dst = filepath.Join(defaultAppDir, dst)
	if runtime.GOOS == "windows" {
		src = src + ".exe"
		dst = dst + ".exe"
	}
	sourceFileStat, err := os.Stat(src)
	if err != nil {
		manager.ServiceLog.WithField("info", src).WithError(err).Error("拷贝源文件异常")
		return
	}
	if !sourceFileStat.Mode().IsRegular() {
		manager.ServiceLog.WithField("info", src).Error("拷贝源文件不是有效文件")
		return
	}
	source, err := os.Open(src)
	if err != nil {
		manager.ServiceLog.WithField("info", src).WithError(err).Error("拷贝源文件读取失败")
		return
	}
	defer source.Close()
	destination, err := os.Create(dst)
	if err != nil {
		manager.ServiceLog.WithField("info", dst).WithError(err).Error("拷贝目标文件创建失败")
		return
	}
	defer destination.Close()
	buf := make([]byte, 2048)
	for {
		n, err := source.Read(buf)
		if err != nil && err != io.EOF {
			manager.ServiceLog.WithField("info", dst).WithError(err).Error("拷贝源文件读取异常")
			return
		}
		if n == 0 {
			break
		}
		if _, err := destination.Write(buf[:n]); err != nil {
			manager.ServiceLog.WithError(err).Error("拷贝目标文件写入异常")
			return
		}
	}
	os.Chmod(dst, 0755)
}

// 更新运行程序
func doUpdateProcessExec() error {
	if runProcess == nil {
		return errors.New("process is nil")
	}
	isStart := runProcess.IsStart
	// 先检测和服务端的最新版本是否一致,一致就无需处理
	if serverVersion == "" { //获取不到服务器版本
		return errors.New("服务端运行运行版本获取失败")
	}
	if runProcess.version() != serverVersion {
		// 删除原来文件
		oldFileName := runProcess.cfg.Name
		if runtime.GOOS == "windows" && !strings.HasSuffix(oldFileName, ".exe") {
			oldFileName = oldFileName + ".exe"
		}
		e := os.Remove(filepath.Join(defaultAppDir, oldFileName))
		if e != nil && !os.IsNotExist(e) {
			manager.ServiceLog.WithError(e).Error("原文件移除失败")
			return e
		}
		err := downExecFile(runProcess.cfg.Name, serverVersion)
		if err != nil {
			manager.ServiceLog.WithError(err).Error("从服务的下载失败")
			return errors.New("服务端软件下载失败")
		}
	}
	manager.ServiceLog.Info("开始结束进程")
	// 停止应用
	runProcess.Stop()
	manager.ServiceLog.Info("开始替换运行文件")
	// 替换原来的运行文件
	cp(runProcess.cfg.Name, runProcess.cfg.id)
	// 替换版本信息
	runProcess.cfg.Ver = runProcess.version()
	if runProcess.cfg.Ver != serverVersion {
		manager.ServiceLog.Info("运行文件替换失败")
		return errors.New("执行文件更新失败")
	}
	manager.ServiceLog.Info("运行文件已替换")
	// 重新启动应用
	if isStart {
		return runProcess.Start()
	}
	return nil
}
